fix(desktop): a manual mirror or sync no longer hangs on a silent spinner - #30
Merged
Conversation
…nner Pressing Preview -- or Sync with Mirror armed -- opened the dialog on a spinner and "Scanning both sides…", and that is all it ever showed. Three separate things made that spinner the whole experience. The scan reported nothing. `previewTransfer` awaited the dry run to completion and only then returned, so for however long a full walk of both trees takes -- minutes from a laptop to a server over a WAN -- there was no count, no path, no clock. A scan that was working and one that had wedged looked exactly alike, which is why a slow preview read as a hang. The result was enormous and unused. Every itemized change was accumulated and returned. A dry run of one ordinary source tree here emitted 332,303 of them: ~71MB of `changes` crossing the IPC boundary by structured clone, for a field the renderer never read. It only ever used the summary and the delete list. Nothing was bounded or stoppable. The delete list rendered one DOM node per entry, so a first mirror into an empty destination asked Chromium for hundreds of thousands of rows. And Cancel only hid the dialog: the dry run carried on to the end, uninterruptible, then resolved into a window nobody was looking at. Now: - Progress streams while the scan runs, over a new `event:preview` channel. The dialog shows rsync's own `to-chk` counters, the changes and deletions found so far, the path being compared, and the elapsed time. `total` grows during the run because rsync builds its file list incrementally, so it is presented as an estimate rather than a deadline. - The result carries counts, not changes. `deleteTotal` and `changeTotal` are exact; the enumerated `deletes` stop at 5,000 and the list says how many more there are. The confirm button always counts the true total, so a truncated list never understates what a mirror will remove. - A preview is registered under an id the renderer chooses, so Stop scanning kills the rsync process. Starting a second preview supersedes the first, and a result whose id is stale is discarded -- otherwise a superseded scan could paint its delete list over a different pair's route. - A cancelled scan is never `ok`. It has not established that anything is safe to delete, and `ok` is what gates the confirm. Two things found by driving the real widget rather than reading it: - The delete list did not clip. `max-h-[210px]` sat on the ScrollArea root, whose viewport is `size-full`; with no definite height it grew to fit and the rows printed over the disclosure below. It is now `min-h-0 flex-1` inside a bounded flex column, like every other ScrollArea in the app. - The footer rendered throughout the scan, so a full-strength Mirror button sat under the spinner offering to run a mirror whose delete list did not exist yet. There are no confirm controls until there is something to confirm, and focus moves to Cancel when the result arrives -- `initialFocus` alone stopped being enough once the footer waits. Verified in headless Chromium against the export served with the app's real CSP, at 1360x860 and at the 960x600 minimum, in both themes. Three regression tests cover the streaming, the cap, and the cancel; each was confirmed to fail with its bug reintroduced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VScug5VRbcTuhiAoieeQ52
`cancelAll` on before-quit stopped transfers but not previews, so closing the window mid-scan left an rsync and an ssh walking a remote tree with nothing left to report to. A dry run is read-only, which is why it went unnoticed, but it is still two processes and a remote session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VScug5VRbcTuhiAoieeQ52
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pressing Preview — or Sync with Mirror armed — opened the dialog on a spinner and "Scanning both sides…", and that is all it ever showed. Reported as
dev → This computerhanging with no feedback and no progress bar.Three separate things made that spinner the whole experience.
The scan reported nothing.
previewTransferawaited the dry run to completion and only then returned, so for however long a full walk of both trees takes — minutes from a laptop to a server over a WAN — there was no count, no path, no clock. A scan that was working and one that had wedged looked exactly alike.The result was enormous and unused. Every itemized change was accumulated and returned. A dry run of one ordinary source tree emitted 332,303 of them: ~71MB of
changescrossing the IPC boundary by structured clone, for a field the renderer never read — it only ever used the summary and the delete list.Nothing was bounded or stoppable. The delete list rendered one DOM node per entry, so a first mirror into an empty destination asked Chromium for hundreds of thousands of rows. And Cancel only hid the dialog: the dry run carried on to the end, uninterruptible, then resolved into a window nobody was looking at.
What changed
event:previewchannel. The dialog shows rsync's ownto-chkcounters, changes and deletions found so far, the path being compared, and elapsed time.totalgrows during the run because rsync builds its file list incrementally, so it is presented as an estimate rather than a deadline.deleteTotalandchangeTotalare exact; the enumerateddeletesstop at 5,000 and the list says how many more there are. The confirm button always counts the true total, so a truncated list never understates what a mirror will remove.ok. It has not established that anything is safe to delete, andokis what gates the confirm.Found by driving the widget, not reading it
max-h-[210px]sat on the ScrollArea root, whose viewport issize-full; with no definite height it grew to fit and the rows printed over the disclosure below. Nowmin-h-0 flex-1inside a bounded flex column, like every other ScrollArea in the app.initialFocusalone stopped being enough once the footer waits.Verification
Driven in headless Chromium against the export served with the app's real CSP, at 1360x860 and at the 960x600 minimum, in both themes — no CSP violations, delete rows in the DOM bounded at ~5,050 instead of 332,303.
pnpm test520 passing,pnpm -r typecheckclean,pnpm smoke:desktopall three guards ok. Three regression tests cover the streaming, the cap and the cancel; each was confirmed to fail with its bug reintroduced.🤖 Generated with Claude Code
https://claude.ai/code/session_01VScug5VRbcTuhiAoieeQ52